Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
88.24% |
15 / 17 |
CRAP | |
98.00% |
147 / 150 |
| APC | |
0.00% |
0 / 1 |
|
88.24% |
15 / 17 |
37 | |
98.00% |
147 / 150 |
| collect | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
| updateHistogram | |
100.00% |
1 / 1 |
5 | |
100.00% |
16 / 16 |
|||
| updateGauge | |
100.00% |
1 / 1 |
4 | |
100.00% |
12 / 12 |
|||
| updateCounter | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
| flushAPC | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| metaKey | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| valueKey | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
| histogramBucketValueKey | |
100.00% |
1 / 1 |
1 | |
100.00% |
7 / 7 |
|||
| metaData | |
100.00% |
1 / 1 |
1 | |
100.00% |
5 / 5 |
|||
| collectCounters | |
100.00% |
1 / 1 |
3 | |
100.00% |
17 / 17 |
|||
| collectGauges | |
100.00% |
1 / 1 |
3 | |
100.00% |
17 / 17 |
|||
| collectHistograms | |
100.00% |
1 / 1 |
6 | |
100.00% |
42 / 42 |
|||
| toInteger | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| fromInteger | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| sortSamples | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
| encodeLabelValues | |
0.00% |
0 / 1 |
2.06 | |
75.00% |
3 / 4 |
|||
| decodeLabelValues | |
0.00% |
0 / 1 |
3.21 | |
71.43% |
5 / 7 |
|||
| <?php | |
| declare(strict_types = 1); | |
| namespace Prometheus\Storage; | |
| use Prometheus\MetricFamilySamples; | |
| use RuntimeException; | |
| class APC implements Adapter | |
| { | |
| const PROMETHEUS_PREFIX = 'prom'; | |
| /** | |
| * @return MetricFamilySamples[] | |
| */ | |
| public function collect() : array | |
| { | |
| $metrics = $this->collectHistograms(); | |
| $metrics = array_merge($metrics, $this->collectGauges()); | |
| $metrics = array_merge($metrics, $this->collectCounters()); | |
| return $metrics; | |
| } | |
| public function updateHistogram(array $data) : void | |
| { | |
| // Initialize the sum | |
| $sumKey = $this->histogramBucketValueKey($data, 'sum'); | |
| $new = apcu_add($sumKey, $this->toInteger(0)); | |
| // If sum does not exist, assume a new histogram and store the metadata | |
| if ($new) { | |
| apcu_store($this->metaKey($data), json_encode($this->metaData($data))); | |
| } | |
| // Atomically increment the sum | |
| // Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/value.go#L91 | |
| $done = false; | |
| while (!$done) { | |
| $old = apcu_fetch($sumKey); | |
| $done = apcu_cas($sumKey, $old, $this->toInteger($this->fromInteger($old) + $data['value'])); | |
| } | |
| // Figure out in which bucket the observation belongs | |
| $bucketToIncrease = '+Inf'; | |
| foreach ($data['buckets'] as $bucket) { | |
| if ($data['value'] <= $bucket) { | |
| $bucketToIncrease = $bucket; | |
| break; | |
| } | |
| } | |
| // Initialize and increment the bucket | |
| apcu_add($this->histogramBucketValueKey($data, $bucketToIncrease), 0); | |
| apcu_inc($this->histogramBucketValueKey($data, $bucketToIncrease)); | |
| } | |
| public function updateGauge(array $data) : void | |
| { | |
| $valueKey = $this->valueKey($data); | |
| if ($data['command'] == Adapter::COMMAND_SET) { | |
| apcu_store($valueKey, $this->toInteger($data['value'])); | |
| apcu_store($this->metaKey($data), json_encode($this->metaData($data))); | |
| } else { | |
| $new = apcu_add($valueKey, $this->toInteger(0)); | |
| if ($new) { | |
| apcu_store($this->metaKey($data), json_encode($this->metaData($data))); | |
| } | |
| // Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/value.go#L91 | |
| $done = false; | |
| while (!$done) { | |
| $old = apcu_fetch($valueKey); | |
| $done = apcu_cas($valueKey, $old, $this->toInteger($this->fromInteger($old) + $data['value'])); | |
| } | |
| } | |
| } | |
| public function updateCounter(array $data) : void | |
| { | |
| $new = apcu_add($this->valueKey($data), 0); | |
| if ($new) { | |
| apcu_store($this->metaKey($data), json_encode($this->metaData($data))); | |
| } | |
| apcu_inc($this->valueKey($data), $data['value']); | |
| } | |
| public function flushAPC() : void | |
| { | |
| apcu_clear_cache(); | |
| } | |
| /** | |
| * @param array $data | |
| * @return string | |
| */ | |
| private function metaKey(array $data) : string | |
| { | |
| return implode(':', [self::PROMETHEUS_PREFIX, $data['type'], $data['name'], 'meta']); | |
| } | |
| /** | |
| * @param array $data | |
| * @return string | |
| */ | |
| private function valueKey(array $data) : string | |
| { | |
| return implode(':', [ | |
| self::PROMETHEUS_PREFIX, | |
| $data['type'], | |
| $data['name'], | |
| $this->encodeLabelValues($data['labelValues']), | |
| 'value' | |
| ]); | |
| } | |
| /** | |
| * @param array $data | |
| * @return string | |
| */ | |
| private function histogramBucketValueKey(array $data, $bucket) : string | |
| { | |
| return implode(':', [ | |
| self::PROMETHEUS_PREFIX, | |
| $data['type'], | |
| $data['name'], | |
| $this->encodeLabelValues($data['labelValues']), | |
| $bucket, | |
| 'value' | |
| ]); | |
| } | |
| /** | |
| * @param array $data | |
| * @return array | |
| */ | |
| private function metaData(array $data) : array | |
| { | |
| $metricsMetaData = $data; | |
| unset($metricsMetaData['value']); | |
| unset($metricsMetaData['command']); | |
| unset($metricsMetaData['labelValues']); | |
| return $metricsMetaData; | |
| } | |
| /** | |
| * @return array | |
| */ | |
| private function collectCounters() : array | |
| { | |
| $counters = []; | |
| foreach (new \APCUIterator('/^prom:counter:.*:meta/') as $counter) { | |
| $metaData = json_decode($counter['value'], true); | |
| $data = [ | |
| 'name' => $metaData['name'], | |
| 'help' => $metaData['help'], | |
| 'type' => $metaData['type'], | |
| 'labelNames' => $metaData['labelNames'], | |
| ]; | |
| foreach (new \APCUIterator('/^prom:counter:' . $metaData['name'] . ':.*:value/') as $value) { | |
| $parts = explode(':', $value['key']); | |
| $labelValues = $parts[3]; | |
| $data['samples'][] = [ | |
| 'name' => $metaData['name'], | |
| 'labelNames' => [], | |
| 'labelValues' => $this->decodeLabelValues($labelValues), | |
| 'value' => $value['value'] | |
| ]; | |
| } | |
| $this->sortSamples($data['samples']); | |
| $counters[] = new MetricFamilySamples($data); | |
| } | |
| return $counters; | |
| } | |
| /** | |
| * @return array | |
| */ | |
| private function collectGauges() : array | |
| { | |
| $gauges = []; | |
| foreach (new \APCUIterator('/^prom:gauge:.*:meta/') as $gauge) { | |
| $metaData = json_decode($gauge['value'], true); | |
| $data = [ | |
| 'name' => $metaData['name'], | |
| 'help' => $metaData['help'], | |
| 'type' => $metaData['type'], | |
| 'labelNames' => $metaData['labelNames'], | |
| ]; | |
| foreach (new \APCUIterator('/^prom:gauge:' . $metaData['name'] . ':.*:value/') as $value) { | |
| $parts = explode(':', $value['key']); | |
| $labelValues = $parts[3]; | |
| $data['samples'][] = [ | |
| 'name' => $metaData['name'], | |
| 'labelNames' => [], | |
| 'labelValues' => $this->decodeLabelValues($labelValues), | |
| 'value' => $this->fromInteger($value['value']) | |
| ]; | |
| } | |
| $this->sortSamples($data['samples']); | |
| $gauges[] = new MetricFamilySamples($data); | |
| } | |
| return $gauges; | |
| } | |
| /** | |
| * @return array | |
| */ | |
| private function collectHistograms() : array | |
| { | |
| $histograms = []; | |
| foreach (new \APCUIterator('/^prom:histogram:.*:meta/') as $histogram) { | |
| $metaData = json_decode($histogram['value'], true); | |
| $data = [ | |
| 'name' => $metaData['name'], | |
| 'help' => $metaData['help'], | |
| 'type' => $metaData['type'], | |
| 'labelNames' => $metaData['labelNames'], | |
| 'buckets' => $metaData['buckets'] | |
| ]; | |
| // Add the Inf bucket so we can compute it later on | |
| $data['buckets'][] = '+Inf'; | |
| $histogramBuckets = []; | |
| foreach (new \APCUIterator('/^prom:histogram:' . $metaData['name'] . ':.*:value/') as $value) { | |
| $parts = explode(':', $value['key']); | |
| $labelValues = $parts[3]; | |
| $bucket = $parts[4]; | |
| // Key by labelValues | |
| $histogramBuckets[$labelValues][$bucket] = $value['value']; | |
| } | |
| // Compute all buckets | |
| $labels = array_keys($histogramBuckets); | |
| sort($labels); | |
| foreach ($labels as $labelValues) { | |
| $acc = 0; | |
| $decodedLabelValues = $this->decodeLabelValues($labelValues); | |
| foreach ($data['buckets'] as $bucket) { | |
| $bucket = (string) $bucket; | |
| if (!isset($histogramBuckets[$labelValues][$bucket])) { | |
| $data['samples'][] = [ | |
| 'name' => $metaData['name'] . '_bucket', | |
| 'labelNames' => ['le'], | |
| 'labelValues' => array_merge($decodedLabelValues, [$bucket]), | |
| 'value' => $acc | |
| ]; | |
| } else { | |
| $acc += $histogramBuckets[$labelValues][$bucket]; | |
| $data['samples'][] = [ | |
| 'name' => $metaData['name'] . '_' . 'bucket', | |
| 'labelNames' => ['le'], | |
| 'labelValues' => array_merge($decodedLabelValues, [$bucket]), | |
| 'value' => $acc | |
| ]; | |
| } | |
| } | |
| // Add the count | |
| $data['samples'][] = [ | |
| 'name' => $metaData['name'] . '_count', | |
| 'labelNames' => [], | |
| 'labelValues' => $decodedLabelValues, | |
| 'value' => $acc | |
| ]; | |
| // Add the sum | |
| $data['samples'][] = [ | |
| 'name' => $metaData['name'] . '_sum', | |
| 'labelNames' => [], | |
| 'labelValues' => $decodedLabelValues, | |
| 'value' => $this->fromInteger($histogramBuckets[$labelValues]['sum']) | |
| ]; | |
| } | |
| $histograms[] = new MetricFamilySamples($data); | |
| } | |
| return $histograms; | |
| } | |
| /** | |
| * @param mixed $val | |
| * @return int | |
| */ | |
| private function toInteger($val) : int | |
| { | |
| return unpack('Q', pack('d', $val))[1]; | |
| } | |
| /** | |
| * @param mixed $val | |
| * @return float | |
| */ | |
| private function fromInteger($val) : float | |
| { | |
| return unpack('d', pack('Q', $val))[1]; | |
| } | |
| private function sortSamples(array &$samples) | |
| { | |
| usort($samples, function($a, $b){ | |
| return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); | |
| }); | |
| } | |
| /** | |
| * @param array $values | |
| * @return string | |
| * @throws RuntimeException | |
| */ | |
| private function encodeLabelValues(array $values) : string | |
| { | |
| $json = json_encode($values); | |
| if (false === $json) { | |
| throw new RuntimeException(json_last_error_msg()); | |
| } | |
| return base64_encode($json); | |
| } | |
| /** | |
| * @param string $values | |
| * @return array | |
| * @throws RuntimeException | |
| */ | |
| private function decodeLabelValues($values) : array | |
| { | |
| $json = base64_decode($values, true); | |
| if (false === $json) { | |
| throw new RuntimeException('Cannot base64 decode label values'); | |
| } | |
| $decodedValues = json_decode($json, true); | |
| if (false === $decodedValues) { | |
| throw new RuntimeException(json_last_error_msg()); | |
| } | |
| return $decodedValues; | |
| } | |
| } |